<?php
/**
 * scottlib
 * ORM lite for MySQL, and data buckets for MySQL and Amazon SimpleDB
 * @author Aaron Collegeman aaroncollegeman.com
 * @version 0.1.3 (alpha): $Id: scottlib.php 160 2009-01-12 13:09:39Z acollegeman $
 *
 * Copyright (C)2008 Collegeman.net, LLC.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 */

// ----------------------------------------------------------------------------
// Don't touch anything in this file.  That way when there's an update, you
// can just overwrite this file with the new one.  Go ahead, we warned you!
// ----------------------------------------------------------------------------

$scottlib_include_at = microtime();

if (!defined('SIMPLEDB_MAX_NEGATIVE_VALUE'))
	define('SIMPLEDB_MAX_NEGATIVE_VALUE', -1000000);

if (!defined('SIMPLEDB_MAX_POSITIVE_VALUE')) 
	define('SIMPLEDB_MAX_POSITIVE_VALUE', 2147483647+SIMPLEDB_MAX_NEGATIVE_VALUE);

@include_once('scottlib-cfg.php');
	
class slMySqlResultSet implements Iterator {
	
	private $__resource;
	
	private $__position = 0;
	
	private $__active = null;
	
	private $__mode = 'object';
	
	function __construct(/* mysql result set */ $resource) {
		$this->__resource = $resource;
	}
	
	function rewind() {
		@mysql_data_seek($this->__resource, 0);
		$this->next();
		$this->__position = 0;
	}
	
	function current() {
		return $this->__active;
	}
	
	function valid() {
		return ($this->__active !== null && $this->__active !== false);	
	}
	
	function key() {
		return $this->__position;	
	}
	
	function next() {
		if ($this->__mode == 'object') 
			$this->__active = @mysql_fetch_object($this->__resource);
			
		else if ($this->__mode == 'assoc')
			$this->__active = @mysql_fetch_assoc($this->__resource);
			
		else if ($this->__mode == 'array')
			$this->__active = @mysql_fetch_array($this->__resource);
			
		$this->__position++;
	}
	
	function objects() {
		$this->__mode = 'object';	
		return $this;
	}
	
	/**
 	 * @deprecated Use slMySqlResultSet::hash() instead.
	 */
	function assocs() {
		return $this->hash();
	}
	
	function hash() {
		$this->__mode = 'assoc';	
		return $this;
	}
	
	function arrays() {
		$this->__mode = 'array';
		return $this;
	}
	
	function count() {
		return @mysql_num_rows($this->__resource);
	}	
	
	function fieldCount() {
		return @mysql_num_fields($this->__resource);	
	}
	
	function first() {
		if ($this->count() >= 1) {
			$this->rewind();
			$this->next();
			return $this->current();
		}
		else
			return null;
	}
	
}

class slMySqlConn {

	private $__host;
	
	private $__username;
	
	private $__password;
	
	private $__database;
	
	private $__conn;
	
	private $__transactionLevel = 0;
	
	private static $__instance;
	
	function __construct($server, $username, $password, $database = null, $client_flags = null) {
		ob_start();
		$this->__conn = mysql_connect($server, $username, $password, true, $client_flags);
		$error = ob_get_clean();
		
		if ($this->__conn === false)
			throw new Exception("Failed to connect to server, $username:".preg_replace('/./', '*', $password)."@$server. ".trim($error));
		
		if (!empty($database))
			$this->database($database);
			
		self::$__instance = $this;
	}
	
	static function instance() {
		if (self::$__instance == null)
			throw new Exception("You haven't created an instance of slMySQLConn yet; can't return a value through slMySQLConn::instance until you do.");
		return self::$__instance;
	}
	
	private static function escapeArray($arr) {
		$values = array();
		foreach ($arr as $k => $v) {
			if (is_numeric($v))
				$values[] = $v;
			else if (is_object($v) || is_array($v))
				$values[] = "'".mysql_escape_string(serialize($v))."'";
			else
				$values[] = "'".mysql_escape_string($v)."'";
		}
		return "(".join(",", $values).")";
	}
	
	private static function punch($sql, $value, $offset, $placeholder_length=1) {
		return substr($sql, 0, $offset).$value.substr($sql, $offset+$placeholder_length);
	}
	
	static function prepare($sql, $params = array()) {
		if (!is_object($params) && !is_array($params))
			$params = array($params);
		
		// generic parameter mode: 
		// ? (possibly escaped) and # (escaped with backticks) are placeholders, strings are escaped automatically
		// and %d or %n (numeric), %c (date), %s (string) %a (array), and %p (pass-through), escaping dependent upon parameter type
		if (is_array($params) && slMySqlEngine::allKeysAreNumeric($params)) { 
			
			// FIXME: this string ignores escaped placeholders: ([\?\#](in)?|%[nds])
			if (preg_match_all('/([\?\#]|%[dcsapnb])/', $sql, $matches, PREG_OFFSET_CAPTURE)) { 
				
				$placeholders = $matches[0];
				if (($npl = count($placeholders)) != ($npa = count($params)))
					throw new Exception("Number of parameters ($npa) does not equal number of placeholders ($npl)");
				
				for($i=count($placeholders)-1; $i>=0; $i--) {
					
					list($placeholder, $offset) = $placeholders[$i];
					$value = $params[$i];

					// value is null
					if ($value === null)
						$sql = self::punch($sql, 'null', $offset, strlen($placeholder));
					
					// expecting boolean value:
					else if ($placeholder == '%b') {
						if (!is_bool($value) && !is_bool( ($value ? true : false) ))
							throw new Exception("Expecting boolean for parameter $i, found: ".$value);
						$sql = self::punch($sql, ($value ? 1 : 0), $offset, 2);	
					}
						
					// expecting array parameter value:
					else if ($placeholder == '%a') {
						if (!is_array($value))
							throw new Exception("Expecting array for parameter $i, found: ".$value);
						$sql = self::punch($sql, self::escapeArray($value), $offset, 2);
					}
					
					// expecting numeric value:
					else if ($placeholder == '%d' || $placeholder == '%n') {
						if (!is_numeric($value))
							throw new Exception("Expecting numeric for parameter $i, found: ".$value);	
						$sql = self::punch($sql, $value, $offset, 2);
					}
					
					// expecting date value:
					else if ($placeholder == '%c') {
						// timestamp?
						if (is_numeric($value)) {
							if (($date = date('Y/m/d H:i:s', $value)) === false)
								throw new Exception("Failed to create date from timestamp: ".$value);
						}
						
						// strtotime parsable value?
						else { 
							if (($timestamp = strtotime($value)) === false)
								throw new Exception("Failed to parse timestamp from: ".$value);
							$date = date('Y/m/d H:i:s', $timestamp);
						}
						
						$sql = self::punch($sql, "'".$date."'", $offset, 2);
					}
					
					// expecting string value:
					else if ($placeholder == '%s') {
						if (!is_string($value) && !is_numeric($value))
							throw new Exception("Expecting string for parameter $i, found: ".$value);
						$escaped = "'".mysql_escape_string($value)."'";
						$sql = self::punch($sql, $escaped, $offset, 2); 	
					}
					
					// punch-through and numeric parameter values:
					else if ($placeholder == '%p') {
						$sql = self::punch($sql, $value, $offset, 2);	
					}
					
					// no escaping: 
					else if ($placeholder == '#') { 
						$sql = self::punch($sql, "`".$value."`", $offset);
					}
					
					// numeric parameter value:
					else if (is_numeric($value)) {
						$sql = self::punch($sql, $value, $offset);	
					}
					
					// escaping:
					else { 
						$escaped = "'".mysql_escape_string($value)."'";
						$sql = self::punch($sql, $escaped, $offset);
					}
				}
			}
		}
		
		// named parameter mode:
		// parameter denoted by colon (e.g., ":name" is parametered named "name")
		// escaping dependent upon parameter value and placeholder prefix: ":" results in normal escaping, "#" in system escaping with backticks
		else { 
			// FIXME: object property parsing
			
			if (preg_match_all('/(\!)?([\:\#])([\w\_]+)\b/i', $sql, $matches, PREG_OFFSET_CAPTURE)) {
				list($fulltext, $optionals, $types, $placeholders) = $matches;  
				
				/* Can't do this and have nullable parameter placeholders
				if (($npl = count($placeholders)) != ($npa = count($params)))
					throw new Exception("Number of parameters ($npa) does not equal number of placeholders ($npl)");
				*/
				
				for($i=count($placeholders)-1; $i>=0; $i--) {
					list($type) = $types[$i];
					$optional = $optionals[$i];
					list ($placeholder, $offset) = $placeholders[$i];	
					
					// standard named parameter:
					if ($type == ':') {
						
						// optional named parameter: use null in place of missing parameter values
						if (!isset($params[$placeholder])) {
							if ($optional)
								$sql = self::punch($sql, 'null', $offset-2, strlen($placeholder)+2);
							else
								throw new Exception("Parameter value not provided for placeholder [$placeholder].");
						}
						
						// required parameters:
						else {
							$value = $params[$placeholder];
							if (is_numeric($value))
								$sql = self::punch($sql, $value, $offset-1, strlen($placeholder)+1);
							else {
								$sql = self::punch($sql, "'".mysql_escape_string($value)."'", $offset-1, strlen($placeholder)+1);
							}
						}
					}
					
					else if ($type == '#') {
						if (!isset($params[$placeholder]))
							throw new Exception("Parameter value not provided for placeholder [$placeholder].");
						
						$value = $params[$placeholder];
						$sql = self::punch($sql, "`".$value."`", $offset-1, strlen($placeholder)+1);
					}
				}
			}
		}
		
		return $sql;
	}
	
	function database($name) {
		if (!@mysql_select_db($name, $this->__conn))
			throw new Exception("Failed to connect to database $name: ".mysql_error($this->__conn));	
		
		return $this;
	}
	
	function query($sql) {
		if (($resource = @mysql_query($sql, $this->__conn)) === false)
			throw new Exception("Failed to execute query: ".mysql_error($this->__conn));
			
		return new slMySqlResultSet($resource);
	}
	
	function select($sql) {
		return $this->query($sql);	
	}
	
	function val($sql, $default = null) {
		$r = @mysql_query($sql, $this->__conn);
		if ($r === false)
			throw new Exception('Failed to execute query: '.mysql_error($this->__conn));
		else {
			$rs = @mysql_fetch_array($r);
			if ($rs == null)
				return $default;
			else {
				$value = $rs[0];
				return ($value == null ? $default : $value);
			}
		}
	}
	
	function execute($sql) {
		if (!@mysql_query($sql, $this->__conn))
			throw new Exception("Failed to execute query: ".mysql_error($this->__conn));
		
		return mysql_affected_rows($this->__conn);	
	}
	
	function update($sql) {
		return $this->execute($sql);	
	}
	
	function insert($sql) {
		return $this->execute($sql);	
	}
	
	function delete($sql) {
		return $this->execute($sql);
	}	
	
	function begin() {
		$this->__transactionLevel++;
		if ($this->__transactionLevel == 1) {
			$this->execute('SET AUTOCOMMIT=0');
			$this->execute('BEGIN');	
		}
	}
	
	function commit() {
		$this->__transactionLevel--;
		if ($this->__transactionLevel === 0) {
			$this->execute('COMMIT');
			$this->execute('SET AUTOCOMMIT=1');
		}
	}
	
	function rollback() {
		if ($this->__transactionLevel > 0) {
			$this->__transactionLevel = 0;
			$this->execute('ROLLBACK');
			$this->execute('SET AUTOCOMMIT=1');
		}
	}
	
	/**
  	 * @deprecated This is not for production purposes.	 
 	 * FIXME: remove all references to this, and then get rid of it	
	 */
	static function lookAhead($haystack, $needle, $offset=0) {
		if (($c = strpos($haystack, $needle, $offset)) !== false) {
			$chunk = substr($haystack, $c, $c-$offset-1);
			return array($c, $chunk);	
		}
		else
			return false;
	}
	
	/**
  	 * @deprecated This is not for production purposes.	 
 	 * FIXME: remove all references to this, and then get rid of it	
	 */
	static function lookAheadInsensitive($haystack, $needle, $startAt=0) {
		if (($c = stripos($haystack, $needle, $offset)) !== false) {
			$chunk = substr($haystack, $c, $c-$offset-1);
			return array($c, $chunk);	
		}
		else
			return false;
	}
	
}

if (!function_exists('beginTransaction')) {
	function beginTransaction() {
		$instance = slMySqlConn::instance();
		$instance->begin();
	}
}

if (!function_exists('begin')) {
	function begin() {
		$instance = slMySqlConn::instance();
		$instance->begin();
	}
}

if (!function_exists('commit')) {
	function commit() {
		$instance = slMySqlConn::instance();
		$instance->commit();
	}
}

if (!function_exists('rollback')) {
	function rollback() {
		$instance = slMySqlConn::instance();
		$instance->rollback();
	}
}

if (!function_exists('strahead')) {
	function strahead($haystack, $needle, $offset=0) {
		return slMySqlConn::lookAhead($haystack, $needle, $offset);
	}
}

if (!function_exists('striahead')) {
	function striahead($haystack, $needle, $offset=0) {
		return slMySqlConn::lookAheadInsenstive($haystack, $needle, $offset);
	}
}


if (class_exists('sfConfig')) {
	global $sl_db;
	
	if ($config = sfConfig::get('app_scottlib')) {
		$sl_db = new slMySqlConn(
			$config['server'], 
			$config['username'], 
			$config['password'], 
			$config['database'], 
			eval($config['client_flags'])
		);	
	}
}

if (!function_exists('connect')) {
	function connect($server, $username, $password, $database = null, $client_flags = null) {
		return new slMySQLConn($server, $username, $password, $database, $client_flags);
	}
}

if (!function_exists('db')) {
	function db() {
		return slMySQLConn::instance();
	}
}

if (!function_exists('prepare')) {
	function prepare($sql, $params = array()) {
		return slMySqlConn::prepare($sql, $params);
	}
}

if (!function_exists('val')) {
	function val($sql, $default = null) {
		$instance = slMySqlConn::instance();
		return $instance->val($sql, $default);
	}
}

if (!function_exists('query')) {
	function query($sql) {
		$instance = slMySqlConn::instance();
		return $instance->query($sql);
	}
}

if (!function_exists('execute')) {
	function execute($sql) {
		$instance = slMySqlConn::instance();
		return $instance->execute($sql);
	}
}

if (!function_exists('select')) {
	function select($sql) {
		$instance = slMySqlConn::instance();
		return $instance->query($sql);
	}
}

class slReplacement {
	
	private $__value;
	
	function __construct($value) {
		$this->__value = $value;
	}
	
	function getValue() {
		return $this->__value;	
	}
	
}

if (!function_exists('replacement')) {
	function replacement($value) {
		return new slReplacement($value);
	}
}

class slObject {

	/** The name of the underlying table, discovered via annotation (at)table. */
	private $_table;
	
	/** The name of the id field, discovered property annotation (at)id. */
	private $_idProperty;
	
	/** Any binding errors discovered on slObject::bind(Array) call. */
	private $_errors = array();
	
	/** Object relationship state - references to objects defined and loaded via ORM logic. */
	private $_state;
	
	/** Annotation cache. */
	private static $_annotations = array();
	
	/** Reference to slMySqlConn instance. */
	private $_conn;
	
	/**
	 * constructor
	 * @param Array $state Initial object state or null
	 * @param slMySqlConn &$conn An initialized instance of slMySqlConn, or null; when null, slObject will use slMySQLConn::instance() to access the most recently intialized instance.
	 */
	function __construct($state = null, slMySqlConn &$conn = null) {
		$this->_table = self::getClassAnnotation($this, 'table');
		
		$idProperty = self::getIdPropertyName($this);
		$this->_idProperty = $idProperty->getName();
		
		// grab connection object:
		$this->_conn = ($conn != null ? $conn : slMySQLConn::instance());	
			
		if ($state != null && count($state))
			$this->bind($state);
	}
	
	/**
	 * @return The first ReflectionProperty object found to have the annotation (at)id
	 */
	private static function getIdPropertyName(slObject $object) {
		$cache = self::getCachedReflection($object);
		foreach($cache['properties'] as $property) {
			if (self::getPropertyAnnotation($object, $property->getName(), 'id') !== false)
				return $property;				
		}
		
		throw new Exception("You must annotate at least one public/protected property with the @id annotation.");
	}
	
	private function getId() {
		$idProperty = $this->_idProperty;
		return $this->$idProperty;	
	}
	
	private function setId($id) {
		$idProperty = $this->_idProperty;
		$this->$idProperty = $id;
	}
	
	/**
 	 * Persist all data attached to this object.
	 * @param boolean $return When true, method returns the text of the SQL statements for executing the save action; 
	 * otherwise, the method executes the SQL and returns the number of records affected by the save.
	 * @return Refer to $return parameter definition
	 */
	final function save($return = false) {
		
		$names = array();
		$values = array();
		$types = array();
		
		$id = $this->getId();
		
		if ($id)
			$this->onBeforeUpdate($id);
		else
			$this->onBeforeInsert();
		
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty' && $prop->getName() != $this->_idProperty) {
				
				if (!self::hasPropertyAnnotation($this, $prop->getName(), array('subquery', 'transient'))) {
					
					$name = $prop->getName();
					$value = $this->$name;

					if (self::hasPropertyAnnotation($this, $name, 'notNull') && $value === null)
						throw new Exception("Property `$name` is not allowed to be null.");
					
					
					if (self::hasPropertyAnnotation($this, $name, 'integer')) {
						if (strlen($value) && ( !is_numeric($value) || stripos($value, '.') !== false ) )
							throw new Exception("Property `$name` must be an integer: found [$value]");
						else if (!$value && $value !== 0)
							$value = null;
							
						$types[] = '%d';
					}
					
					else if (self::hasPropertyAnnotation($this, $name, 'float') || self::hasPropertyAnnotation($this, $name, 'double')) {
						if ($value && !is_numeric($value))
							throw new Exception("Property `$name` must be a number: found [$value]");
						else if (!$value && $value !== 0)
							$value = null;

						$types[] = '%d';
					}
					
					else if (self::hasPropertyAnnotation($this, $name, 'date')) {
						if (is_numeric($value))
							$value = date('c', $value);
						
						if ($value !== null && strtotime($value) === false)
							throw new Exception("Property `$name` must be a date: found [$value]");
							
						$value = date('Y/m/d H:i:s', strtotime($value));
							
						$types[] = '%s'; // don't use %c here, because %c will force the prepare method to repeat the date translation we just did above...
					}
					
					else if (self::hasPropertyAnnotation($this, $name, 'boolean') || self::hasPropertyAnnotation($this, $name, 'bool')) {
						
						$types[] = '%b';	
						
					}
					
					else {
						if (self::hasPropertyAnnotation($this, $name, 'serialized'))
							$value = serialize($value);	
							
						$types[] = '%s';
					}
					
					
					// last, if @fieldName is present, use instead of $name
					if ($fieldName = self::getPropertyAnnotation($this, $name, 'fieldName'))
						$name = $fieldName;
					
					$names[] = $name;
					$values[] = $value;
				}
			}
		}
		
		// echo '<pre>'.print_r(array($names, $values, $types), true).'</pre>';
		
		if ($id) { // update
			
			$placeholders = array();
			$placeholderValues = array();
			foreach($names as $i => $name) { 
				$placeholders[] = "# = $types[$i]";
				$placeholderValues[] = $name;
				$placeholderValues[] = $values[$i];
			}
			
			$raw = "UPDATE # SET ".join(', ', $placeholders)." WHERE #=%s";
			$sql = slMySqlConn::prepare($raw, array_merge(array($this->_table), $placeholderValues, array($this->_idProperty), array($id))); 
		
			if ($return)
				return $sql;
			else {	
				$affected = $this->_conn->update($sql);
				$this->onAfterUpdate($id);
				return $affected;
			}
			
		}
		
		else { // insert
			
			$namePlacs = array();
			foreach($names as $n)
				$namePlacs[] = "#";
				
			$raw = "INSERT INTO # (".join(', ', $namePlacs).") VALUES (".join(', ', $types).")";
			$sql = slMySqlConn::prepare($raw, array_merge(array($this->_table), $names, $values));

			if ($return)
				return $sql;
			else {
				$affected = $this->_conn->insert($sql);
				$this->setId($this->_conn->val('SELECT @@IDENTITY'));
				$this->onAfterInsert($this->getId());
				return $affected;
			}
			
		}
		
	}
	
	function onBeforeSave($id) {}
	function onAfterSave($id) {}
	function onBeforeInsert() { $this->onBeforeSave(null); }
	function onAfterInsert($id) { $this->onAfterSave($id); }
	function onBeforeUpdate($id) { $this->onBeforeSave($id); }
	function onAfterUpdate($id) { $this->onAfterSave($id); } 
	
	/**
 	 * Accessor magic method, used to alias ORM accessor methods, e.g., $object->foo executes $object->getFoo() 
	 */
	final function __get($name) {
		
	}
	
	/**
	 * Mutator magic method, used to alias ORM mutator methods, e.g., $object->foo = 'value' execute $object->setFoo('value')
	 */
	final function __set($name, $value) {
		
	}
	
	/**
	 * @return Array containing the ReflectionProperty and ReflectionMethod objects that control access to the state of this object.
	 */
	private function getProperties() {
		$cache = self::getCachedReflection($this);
		if (!array_key_exists('slObject.properties', $cache)) {
			$properties = array();
			
			foreach($cache['class']->getProperties() as $property) {
				if (stripos($property->getName(), '_') !== 0 && !$property->isPrivate()) 
					$properties[] = $property;
			}
			
			foreach($cache['class']->getMethods() as $method) {
				if (stripos($method->getName(), '_') !== 0 
					&& $method->getName() != 'getErrors' 
					&& $method->getName() != 'getAll' 
					&& preg_match('/get(.*)/', $method->getName()))
						$properties[] = $method;
			}

			$cache['slObject.properties'] = $properties;
		}
		
		return $cache['slObject.properties'];
	}
	
	private static function &getCachedReflection($class_or_object) {
		$className = (is_string($class_or_object) ? $class_or_object : get_class($class_or_object));
		if (!$className)
			throw new Exception("Failed to load class for $class_or_object");
			
		if (!array_key_exists($className, self::$_annotations)) {
			$class = new ReflectionClass($className);
			$cache = array(
				'class' => 	$class,
				'class_a' => self::getAnnotations($class),
				'properties' => $class->getProperties(),
				'properties_a' => false,
				'methods' => $class->getMethods(),
				'methods_a' => false
			);
			
			// echo '<pre>getCachedReflection: '.print_r($cache, true).'</pre>';
			
			self::$_annotations[$className] = &$cache;
		}
		
		return self::$_annotations[$className];
	}
	
	final static function getAnnotations(/* mixed */ $object) {
		if (in_array(get_class($object), array('ReflectionClass', 'ReflectionMethod', 'ReflectionProperty')))
			$doc = $object->getDocComment();
		else {
			$class = new ReflectionClass(get_class($object));
			$doc = $class->getDocComment();	
		}
		
		$annotations = array();
		if (preg_match_all('/@(\w+) ?(\((.*)\))?/', $doc, $matches)) {
			$names = $matches[1];
			$values = $matches[3];
			foreach($names as $i => $name)
				$annotations[$name] = trim($values[$i]);
		}
		
		return $annotations;
	}
	
	/**
	 * @return The cached ReflectionClass object for $this
	 */		
	private function getClass() {
		$cache = self::getCachedReflection($this);
		return $cache['class'];
	}
	
	public function getTableName() {
		return $this->_table;	
	}
	
	public function getConn() {
		return $this->_conn;	
	}
	
	public function getIdProperty() {
		return $this->_idProperty;	
	}
	
	final static function getClassAnnotation($class_or_object, $annotation) {
		$cache = self::getCachedReflection($class_or_object);
		return (array_key_exists($annotation, $cache['class_a']) ? $cache['class_a'][$annotation] : false);
	}
	
	final static function hasClassAnnotation($class_or_object, $annotation) {
		if (!is_array($annotation))
			$annotation = array($annotation);
		
		foreach($annotation as $a) {
			if (self::getClassAnnotation($class_or_object, $propertyName, $a) !== false)
				return true;
		}
		
		return false;
	}
	
	final static function getMethodAnnotation($class_or_object, $methodName, $annotation) {
		$cache = &self::getCachedReflection($class_or_object);
		if ($cache['methods_a'] === false) {
			$annotations = array();
			foreach($cache['methods'] as $method)
				$annotations[$method->getName()] = self::getAnnotations($method);
				
			$cache['methods_a'] = $annotations;	
			
			//echo '<pre>getMethodAnnotations: '.print_r($annotations, true).'</pre>';
		}
		
		if (!array_key_exists($methodName, $cache['methods_a']))
			throw new Exception("Method $methodName does not exist");
		
		return (array_key_exists($annotation, $cache['methods_a'][$methodName]) ? $cache['methods_a'][$methodName][$annotation] : false);
	}
	
	final static function hasMethodAnnotation($class_or_object, $methodName, $annotation) {
		if (!is_array($annotation))
			$annotation = array($annotation);
		
		foreach($annotation as $a) {
			if (self::getMethodAnnotation($class_or_object, $propertyName, $a) !== false)
				return true;
		}
		
		return false;
	}
	
	final static function getPropertyAnnotation($class_or_object, $propertyName, $annotation) {
		$cache = &self::getCachedReflection($class_or_object);
		
		if (!is_array($cache['properties_a'])) {
			$annotations = array();
			foreach($cache['properties'] as $property) 
				$annotations[$property->getName()] = self::getAnnotations($property);
				
			$cache['properties_a'] = $annotations;	
			
			//echo '<pre>getPropertyAnnotations: '.print_r($annotations, true).'</pre>';
		}
		
		if (!array_key_exists($propertyName, $cache['properties_a']))
			throw new Exception("Property $propertyName does not exist");
		
		return (array_key_exists($annotation, $cache['properties_a'][$propertyName]) ? $cache['properties_a'][$propertyName][$annotation] : false);
	}
	
	final static function hasPropertyAnnotation($class_or_object, $propertyName, $annotation) {
		if (!is_array($annotation))
			$annotation = array($annotation);
		
		foreach($annotation as $a) {
			if (self::getPropertyAnnotation($class_or_object, $propertyName, $a) !== false)
				return true;
		}
			
		return false;
	}
	
	
	/**
     * @param mixed $reflect Either an instance of ReflectionProperty or an instance of ReflectionMethod, from which the name of a property will be derrived.
	 * @return String the name of the property the object represents
	 */
	private static function getPropertyName($reflect) {
		$class = get_class($reflect);
		if ($class == 'ReflectionProperty')
			return $reflect->getName();
		else if ($class == 'ReflectionMethod') {
			if (stripos($method->getName(), '_') !== 0 && preg_match('/get(.*)/', $method->getName(), $matches))
				return self::uncap($matches[1]);	
		}
		else
			throw new Exception("I don't know how to extract a property name from a $class");
	}
	
	/**
	 * @return String an uncapitalized version of $name
	 */
	private static function uncap($name) {
		return ($name != null) ? strtolower(substr($name, 0, 1)).substr($name, 1) : null;
	}
	
	/**
	 * @return String a capitalized version of $name
	 */
	private static function cap($name) {
		return ($name != null) ? strtoupper(substr($name, 0, 1)).substr($name, 1) : null;
	}
	
	/**
	 * Executes query with WHERE and ORDER phrases from $where_and_order, inserting parameters $params, offsetting and limiting with $limit and $offset, and optionally returning the DQL with $return.
	 * @param string $where_and_order
	 * @param Array $params
	 * @param int $limit
	 * @param int $offset
	 * @param boolean $return
	 * @return When records are found, an Array of instantiated and populated instance of your object; otherwise, an empty Array.
	 */
	final function select($where_and_order, $params = array(), $limit = 10, $offset = 0, $return = false) {
		$class = $this->getClass();
		
		$subqueries = array();
		$subqueries[] = '*';
		
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty') {
				if ($q = self::getPropertyAnnotation($this, $prop->getName(), 'subquery')) {
					$prepared = slMySqlConn::prepare($q, array('tableName' => $this->getTableName()));
					$sql = slMySqlConn::prepare("( %p ) AS #", array($prepared, $prop->getName()));
					$subqueries[] = $sql; 
				}
			}
		}
		
		$sql = slMySqlConn::prepare("SELECT %p FROM # %p LIMIT %d OFFSET %d", array(join(',', $subqueries), $this->_table, slMySqlConn::prepare($where_and_order, $params), $limit, $offset));
		
		if ($return)
			return $sql;
		else {
			$objs = array();
			foreach($this->_conn->select($sql)->assocs() as $record) {
				$o = $class->newInstance();
				$o->bind($record);
				$objs[] = $o;
			}
			
			return $objs;
		}
	}
	
	final function selectOne($where_and_order, $params = array(), $return = false) {
		$result = $this->select($where_and_order, $params, 1, 0, $return);
		if ($return)
			return $result; // sql
		else
			return (count($result) ? $result[0] : null);
	}
	
	final function getAll($order = '', $limit=250, $return = false) {
		return $this->select($order, array(), $limit, 0, $return);
	}
	
	final function delete($return = false) {
		if (!($id = $this->getId()) && !$return)
			throw new Exception("Cannot delete record: it isn't connected to the database.");
			
		$this->onBeforeDelete($id);
		$sql = slMySqlConn::prepare("DELETE FROM # WHERE #=%s", array($this->_table, $this->_idProperty, $id));
		if ($return)
			return $sql;
		else {
			$affected = $this->_conn->delete($sql);
			$this->onAfterDelete($id);
			return $affected;
		}
	}
	
	function onBeforeDelete($id) {}
	function onAfterDelete($id) {}
	
	function bind($data = null, $exceptions = array()) {
		//echo '<pre>'.get_class($this).': '.print_r($exceptions, true).'</pre>';
		
		if ($data != null) {
			$this->onBeforeBind($data, $exceptions);
			foreach($this->getProperties() as $prop) {
				if (get_class($prop) == 'ReflectionProperty' && !in_array($prop->getName(), $exceptions)) {
					$name = $prop->getName();
					$isRequired = (self::hasPropertyAnnotation($this, $name, array('notNull', 'required')));
					$isBool = (self::hasPropertyAnnotation($this, $name, array('boolean', 'bool')));
					
					//echo "<div>$name</div>";
					
					// make sure we have a default value for boolean in $data
					if ($isBool && (!array_key_exists($name, $data) || !strlen($data[$name])))
						$data[$name] = false;
						
						
					if (array_key_exists($name, $data)) {
						$value = $data[$name];
						
						// FIXME: This is probably not enough to logically process possible boolean values
						if ($isBool && !is_bool($value)) 
							$value = ord($value);
						
						$this->$name = $value;
						
						$isNumeric = (self::hasPropertyAnnotation($this, $name, array('float', 'double', 'integer')));
						
						if (!strlen($value) && $isRequired)
							$this->addError($name, 'required', 'Required');
							
						if ($isNumeric && !empty($value) && !is_numeric($value))
							$this->addError($name, 'numeric', 'Must be a number');
						
					}
					else if ($isRequired)
						$this->addError($prop->getName(), 'required', 'Required');
				}
			}
			$this->onAfterBind($data, $exceptions);
		}
	}
	
	function onBeforeBind($data, $exceptions) {}
	function onAfterBind($data, $exceptions) {}
	

	/**
 	 * @return A count of errors when binding errors have occurred; otherwise, false.
	 */
	final function hasErrors($property = null) {
		return ($property ? ( array_key_exists($property, $this->_errors) ? count($this->_errors[$property] ) : false) : ( count($this->_errors) ? count($this->_errors) : false ));
	}
	
	/**
	 * @param String $property The name of a property of this method
     * @return When $property is not null, Array(stdClass) when error(s) are found for $property; otherwise, false.  When $property is null, returns all errors.
	 */
	final function getErrors($property = null) {
		return ($property ? ( array_key_exists($property, $this->_errors) ? $this->_errors[$property] : false ) : $this->_errors);
	}
	
	final function error($property) {
		if (($errors = $this->getErrors($property)) !== false) {
			// print_r($errors);
			$error = $errors[0];
			// FIXME: implement error code translation here	
			echo "<span class=\"error\">$error->defaultMessage</span>";
		}
	}
	
	final function checked($property) {
		echo ($this->$property ? 'checked="checked"' : '');	
	}
	
	final function selected($property, $value = null) {
		echo ($this->$property == $value ? 'selected="selected"' : '');	
	}
		
	final function clearErrors() {
		$this->_errors = array();	
	}
	
	final function addError($property, $code = null, $defaultMessage = '') {
		if (!array_key_exists($property, $this->_errors))
			$this->_errors[$property] = array();

		if (!is_array($code))
			$code = array($code);
			
		foreach ($code as $c) {
			$error = new stdClass();
			$error->code = $c;
			$error->defaultMessage = $defaultMessage;
			$this->_errors[$property][] = $error;
		}
	}
	
	/**
	 * Returns $_REQUEST or $_REQUEST[$key] when $key is not null, iff $isFormSubmission !== false and ( $isFormSubmission == true or $_SERVER['REQUEST_METHOD'] == 'POST' ) 
	 * @param string $key
	 * @param boolean isFormSubmission
	 * @return Array or null (refer to method description)
	 */
	static function request($key = null, $isFormSubmission = null) {
		if (self::isFormSubmission($isFormSubmission))
			return ($key != null ? (array_key_exists($key, $_REQUEST) ? $_REQUEST[$key] : null) : $_REQUEST);	
		else
			return null;
	}
	
	static function isFormSubmission($force = null) {
		return $force !== false && ( $force === true || $_SERVER['REQUEST_METHOD'] == 'POST');	
	}
	
	function toArray() {
		$data = array();
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty') {
				$name = $prop->getName();
				$value = $this->$name;
				
				// FIXME: this is a really stupid way to handle nested data:
				if (is_array($value) || is_object($value))
					$value = serialize($value);
	
				$data[$name] = $value;
			}
		}	
		
		$data[$this->_idProperty] = $this->getId();
		return $data;
	}
	
	function serialize() {
		$data = $this->toArray();
		return serialize($data);	
	}
	
	function load($id) {
		
		$subqueries = array();
		$subqueries[] = '*';
		
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty') {
				if ($q = self::getPropertyAnnotation($this, $prop->getName(), 'subquery')) {
					$prepared = slMySqlConn::prepare($q, array('tableName' => $this->getTableName()));
					$sql = slMySqlConn::prepare("( %p ) AS #", array($prepared, $prop->getName()));
					$subqueries[] = $sql; 
				}
			}
		}
		
		$sql = slMySqlConn::prepare("SELECT %p FROM # WHERE #=%s", array(join(',', $subqueries), $this->_table, $this->_idProperty, $id));
		
		$this->onBeforeLoad($id);
		
		$result = $this->_conn->select($sql);
		
		if ($result->count() > 1)
			throw new Exception("Too many records returned for load: ".count($rows));
		else if ($result->count() == 0)
			throw new Exception("No records found with id [$id]");

		foreach($result->assocs() as $record)  // there will be only one
			$this->bind($record);
			
		$this->onAfterLoad($id);
		
		return $this;
	}
	
	function onBeforeLoad($id) {}
	function onAfterLoad($id) {}
	
	function deleteAll($return = false) {
		$sql = slMySqlConn::prepare("DELETE FROM #", $this->_table);
		return ($return ? $sql : $this->_conn->execute($sql)); 		
	}
	
	final static function drop(slObject $obj, $conn = null, $return = false) {
		$tableName = self::getClassAnnotation($obj, 'table');
		if (!$tableName)
			throw new Exception("You must use the @table annotation to tell slObject where to store your data.");
			
		$sql = slMySqlConn::prepare("DROP TABLE IF EXISTS #", $tableName);
		
		if ($conn == null)
			$conn = slMySqlConn::instance();
		
		return ($return ? $sql : $conn->execute($sql));
	}
	
	final static function create(slObject $obj, $conn = null, $return = false) {
		
		$cache = self::getCachedReflection($obj);
		$class = $cache['class'];
		$tableName = self::getClassAnnotation($obj, 'table');
		if (!$tableName)
			$tableName = self::uncap($class->getName());
			
		$sql = array();
		
		$sql[] = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # (", $tableName);
		
		$relationships = array();
		
		$fieldDefs = array();
		
		foreach($cache['properties'] as $prop) {
			
			// skip transient fields:
			if (self::hasPropertyAnnotation($obj, $prop->getName(), 'transient')) continue;
			
			$fieldDef = '/* field name: */ # /* data type: */ %p /* unsigned: */ %p /* zerofill: */ %p /* null: */ %p /* auto_increment: */ %p /* unique: */ %p /* primary: */ %p /* comment: */ %p %p';
			
			$settings = array();
			
			$useAutoIncrement = self::hasPropertyAnnotation($obj, $prop->getName(), 'id') && !self::hasPropertyAnnotation($obj, $prop->getName(), 'noAutoIncrement');
			
			/* field name: */ 
				$settings[] = ($fieldName = self::getPropertyAnnotation($obj, $prop->getName(), 'fieldName') ? $fieldName : $prop->getName());
			
			/* data type: */ 
			// FIXME: complete this with all available MySQL types
				if ($useAutoIncrement)
					$settings[] = 'BIGINT';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'boolean'))
					$settings[] = 'BIT';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'float'))
					$settings[] = 'FLOAT';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'double'))
					$settings[] = 'DOUBLE';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'integer'))
					$settings[] = 'INTEGER';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'date'))
					$settings[] = 'DATETIME';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'text'))
					$settings[] = 'TEXT';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'bigtext'))
					$settings[] = 'BIGTEXT';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'blob'))
					$settings[] = 'BLOB';
				else if (self::hasPropertyAnnotation($obj, $prop->getName(), 'longblob'))
					$settings[] = 'LONGBLOB';
				else if ($exactly = self::getPropertyAnnotation($obj, $prop->getName(), 'exactly'))
					$settings[] = "CHAR($exactly)";
				else if ($maxLength = self::getPropertyAnnotation($obj, $prop->getName(), 'maxLength'))
					$settings[] = "VARCHAR($maxLength)";
				else if ($varchar = self::getPropertyAnnotation($obj, $prop->getName(), 'varchar'))
					$settings[] = "VARCHAR($varchar)";
				else
					$settings[] = 'VARCHAR(1024)';
			
			/* unsigned: */ $settings[] = (self::hasPropertyAnnotation($obj, $prop->getName(), 'unsigned') ? 'UNSIGNED' : '');
					
			/* zerofill: */ $settings[] = (self::hasPropertyAnnotation($obj, $prop->getName(), 'zerofill') ? 'ZEROFILL' : '');
					
			/* null: */ $settings[] = (self::hasPropertyAnnotation($obj, $prop->getName(), 'notNull') ? 'NOT NULL' : 'NULL');
					
			/* auto increment: */ $settings[] = ($useAutoIncrement ? 'AUTO_INCREMENT' : '');
					
			/* unique: */ $settings[] = (self::hasPropertyAnnotation($obj, $prop->getName(), 'unique') ? 'UNIQUE KEY' : '');
					
			/* primary: */ $settings[] = (self::hasPropertyAnnotation($obj, $prop->getName(), 'id') ? 'PRIMARY KEY' : '');
					
			/* comment: */ 
				if ($comment = self::getPropertyAnnotation($obj, $prop->getName(), 'comment')) {
					$settings[] = 'COMMENT';
					$settings[] = "'".mysql_escape_string($comment)."'";	
				}
				else {
					$settings[] = '';
					$settings[] = '';
				}
			
			$fieldDefs[] = slMySqlConn::prepare($fieldDef, $settings);		
			
			if (self::getPropertyAnnotation($obj, $prop->getName(), 'indexed') && !self::getPropertyAnnotation($obj, $prop->getName(), 'unique') && !self::getPropertyAnnotation($obj, $prop->getName(), 'id')) 
				$fieldDefs[] = slMySqlConn::prepare('INDEX(#)', $tableName);
		}
		
		foreach($cache['methods'] as $method) {
			
			$name = $method->getName();
			
			if ($oneToMany = self::getMethodAnnotation($obj, $name, 'oneToMany')) {
				$manyTableName = (self::hasClassAnnotation($oneToMany, 'table') ? self::getClassAnnotation($oneToMany, 'table') : self::uncap($oneToMany));
				$relTableName = (self::hasClassAnnotation($obj, 'table') ? self::getClassAnnotation($obj, 'table') : self::uncap(get_class($obj))).'_'.$manyTableName;
				$ddl = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # ( `oneId` BIGINT UNSIGNED, `manyId` BIGINT UNSIGNED, INDEX(`oneId`), INDEX(`manyId`), INDEX(`oneId`,`manyId`) ) type=InnoDB", $relTableName);
				$relationships[] = $ddl;
			}
			
			else if ($manyToOne = self::getMethodAnnotation($obj, $name, 'manyToOne')) {
				$oneTableName = (self::hasClassAnnotation($manyToOne, 'table') ? self::getClassAnnotation($manyToOne, 'table') : self::uncap($manyToOne));
				$relTableName = $oneTableName.'_'.$obj->_table;
				$ddl = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # ( `oneId` BIGINT UNSIGNED, `manyId` BIGINT UNSIGNED, INDEX(`oneId`), INDEX(`manyId`), INDEX(`oneId`,`manyId`) ) type=InnoDB", $relTableName);
				$relationships[] = $ddl;
			}
			
			else if ($oneToOne = self::getMethodAnnotation($obj, $name, 'oneToOne')) {
				$oneTableName = (self::hasClassAnnotation($oneToOne, 'table') ? self::getClassAnnotation($oneToOne, 'table') : self::uncap($oneToOne));
				$fieldDefs[] = slMySqlConn::prepare("# BIGINT UNSIGNED, INDEX(#)", array($oneTableName."Id", $oneTableName."Id"));
			}
			
			else if ($manyToMany = self::getMethodAnnotation($obj, $name, 'manyToMany')) {
				$manyTableName = (self::hasClassAnnotation($manyToMany, 'table') ? self::getClassAnnotation($manyToMany, 'table') : self::uncap($manyToMany));
				$names = array($obj->_table, $manyTableName);
				sort($names);
				$relTableName = "$names[0]_$names[1]";
				$ddl = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # ( # BIGINT UNSIGNED, # BIGINT UNSIGNED, INDEX(#), INDEX(#), INDEX(#,#) ) type=InnoDB", array($relTableName, $names[0].'Id', $names[1].'Id', $names[0].'Id', $names[1].'Id', $names[0].'Id', $names[1].'Id'));
				$relationships[] = $ddl;
			}
			
		}
		
		$sql[] = join(",\n", $fieldDefs);
		
		$tableType = ($tableType = self::getClassAnnotation($obj, 'tableType') ? $tableType : 'InnoDB');
		
		$sql[] = ") type=$tableType";
		
		$sql = join("\n", $sql);
		
		// echo '<pre>'.$sql.'</pre>';
		
		if (!$return) {
			if ($conn == null)
				$conn = slMySqlConn::instance();

			$affected = $conn->execute($sql);

			foreach($relationships as $r)
				$affected += $conn->execute($r);
				
			return $affected;
		}
		
		else
			return $sql.(count($relationships) ? "\n".join("\n", $relationships) : "");
			
	}
	
	final static function count(slObject $obj) {
		$conn = $obj->getConn();
		return $conn->val(slMySqlConn::prepare('SELECT COUNT(#) FROM #', array($obj->getIdProperty(), $obj->getTableName()))); 		
	}
	
	/**
	 * Properly escapes property value $property using PHP htmlentities function
	 * @param string $property The name of the value to retrieve
	 * @param boolean $return
	 * @return When $return is true, returns the value; otherwise, echos it to the output
	 */
	final function html($property, $return = false) {
		if ($return)
			return htmlentities($this->$property);
		else
			echo htmlentities($this->$property);
	}
	
	final function text($property, $return = false) {
		if ($return)
			return $this->$property;	
		else
			echo $this->$property;
	}
	
	final function money($property, $return = false) {
		$value = $this->$property;
		if ($value) {
			$value = money_format('%i', $this->$property);
			if ($return) 
				$value;
			else
				echo $value;
		}
		else {
			if ($return)
				return null;
			else
				echo '';
		}		
	}
	

} // end slObject

class slData {
	
	private $__domain = null;
	
	private $__itemName = null;
	
	private $__system = null;
	
	private static $__systems = array();
	
	private static $__engines = array();
	
	private static $__options = array();
	
	private $__data = array();
	
	function __construct($domain, $itemName = null, $system = 'default') {
		$this->__domain = $domain;
		$this->__itemName = $itemName;
		$this->__system = $system;
	}
	
	function id() {
		return $this->__itemName;	
	}
	
	function itemName() {
		return $this->__itemName;	
	}
	
	function getItemName() {
		return $this->__itemName;	
	}
	
	function update() {
		foreach($this->__data as $name => $value) {
			if (get_class($value) !== 'slReplacement')
				$this->__data[$name] = new slReplacement($value);
		}	
		$this->save();
	}
	
	private static function getEngine($system) {
		if (!isset(self::$__engines[$system])) {
			if (!isset(self::$__systems[$system])) {
				if ($system == 'default') {

					// FIXME: do sfConfig detection here... if at all.
					throw new Exception("You must configure at least one data system before using scottlib.");
						
				}
				else
					throw new Exception("You didn't configure a data system named [$system].");
			}
			
			if (self::$__systems[$system]['type'] == 'mysql')
				self::$__engines[$system] = new slMySqlEngine(self::$__systems[$system]);
			else if (self::$__systems[$system]['type'] == 'simpledb')
				self::$__engines[$system] = new slSimpleDbEngine(self::$__systems[$system]);
			else
				throw new Exception("Invalid state: unrecognized data system type, [$system].");
		}
		
		return self::$__engines[$system];
	}
	
	function getValues() {
		$values = array();
		foreach ($this->__data as $name => $value) {
			$v = $value;
			
			$v = (is_bool($v)) ? (int) $v : $v;
			
			if (strpos($name, '__') === false)
				$values[$name] = $v;	
		}
		return $values;	
	}
	
	function save() {
		$engine = slData::getEngine($this->__system);
		$this->__itemName = $engine->put($this->__domain, $this->__itemName, $this->getValues());
	}
	
	function delete() {
		
	}
	
	static function deleteDomain($domain, $system = 'default') {
		$engine = slData::getEngine($system);
		$engine->deleteDomain($domain);
	}
	
	static function createDomain($domain, $system = 'default') {
		if (preg_match('/[^a-zA-Z0-9_\-\.]/', $domain) || strlen($domain) < 3 || strlen($domain) > 255)
			throw new Exception("Domain may contain only a-z, A-Z, 0-9, '_', '-', and '.' and must be between 3 and 255 characters in length.");
		
		$engine = slData::getEngine($system);
		$engine->createDomain($domain);	
	}
	
	function __set($name, $value) {
		if ($name == 'itemName' || $name == 'id')
			$this->__itemName = $value;
		else 
			$this->__data[$name] = $value;
	}
	
	function __get($name) {
		if ($name == 'itemName' || $name == 'id')
			return $this->__itemName;
		else 
			return $this->__data[$name];
	}
	
	static function addSystem($options = array()) {
		if (!isset($options['name']))
			$options['name'] = 'default';
		
		if (!isset($options['type']) || !in_array($options['type'], array('mysql', 'simpledb')))
			throw new Exception("System type must be either 'mysql' or 'simpledb'");

		if ($options['type'] == 'mysql') {
			// FIXME: finish validating $options
		}
		
		self::$__systems[$options['name']] = $options;
	}
	
	static function getSystems() {
		return self::$__systems;	
	}
	
}

interface slEngine {
	
	function put($domain, $itemName, $values = array());
	function delete($domain, $itemName);
	function get($domain, $id, $attributes);
	function createDomain($domain);
	function deleteDomain($domain);
	function meta($domain);
	function listDomains();
	function query($query);
	function select($select);
	function queryWithAttributes($domain, $attributes);
	
}

abstract class slBaseEngine implements slEngine {
	
	protected function parseSelect($select) {
		
	}
	
	protected function parseQuery($query) {
		
	}
	
}

class slMySqlEngine extends slBaseEngine  {
	
	private $__conn;
	
	function __construct($options) {
		$this->__conn = new slMySqlConn($options['host'], $options['user'], $options['pass'], $options['database']);
	}
	
	private function buildSafeInsert($itemName, $name, $value) {
		if (is_array($value) || is_object($value)) {
			$serialized = utf8_encode('$php.serialized='.serialize($value));	
			if (strlen($serialized) > 1024)
				throw new Exception("Serialized value for property [$name] is too long: must be fewer than 1024 characters.");
			return slMySqlConn::prepare("(%s, %s, %s)", array($itemName, $name, $serialized));
		}
		else {
			$value = utf8_encode($value);
			if (strlen($value) > 1024)
				throw new Exception("Value for property [$name] is too long: must be fewer than 1024 characters.");
			return slMySqlConn::prepare("(%s, %s, ?)", array($itemName, $name, $value));
		}
	}
	
	public function allKeysAreNumeric($arr) {
		if (!is_array($arr))
			throw new Exception("Parameter to clMySqlEngine::allKeysAreNumeric must be an array!");
		
		$allKeysAreNumeric = true;
		foreach(array_keys($arr) as $key) {
			if (!is_numeric($key)) {
				$allKeysAreNumeric = false;
				break;
			}	
		}

		return $allKeysAreNumeric;
	}
	
	private function generateSql($domain, $itemUniq, &$values, &$deletes, &$inserts) {
		
		$dataTable = "scottlib_".$domain."_data";
		
		foreach ($values as $name => $value) {
			
			$encname = utf8_encode($name);
			
			if (strlen($encname) > 1024)
				throw new Exception("Property name [$encname] must be fewer than 1024 characters.");
			
			if (is_array($value)) {
				if ($this->allKeysAreNumeric($value)) {
					foreach($value as $i => $v)
						$inserts[] = $this->buildSafeInsert($itemUniq, $encname, $v);
				}
				else
					$inserts[] = $this->buildSafeInsert($itemUniq, $encname, $value);	
			}
			else if (is_object($value) && get_class($value) != 'slReplacement') {
				$inserts[] = $this->buildSafeInsert($itemUniq, $encname, $value);	
			}
			else if (get_class($value) == 'slReplacement') {
				$deletes[] = slMySqlConn::prepare("DELETE FROM # WHERE `item_uniq` = %s AND `attribute_name` = %s", array(
					$dataTable,
					$itemUniq,
					$encname
				));
				
				$replacementValue = $value->getValue();
				
				if (is_array($replacementValue) && ($this->allKeysAreNumeric($replacementValue))) {
					foreach($replacementValue as $i => $v)
						$inserts[] = $this->buildSafeInsert($itemUniq, $encname, $v);
				}
				else
					$inserts[] = $this->buildSafeInsert($itemUniq, $encname, $replacementValue);
			}
			else {
				$inserts[] = $this->buildSafeInsert($itemUniq, $encname, $value);
			}
				
		}
		
	}
	
	function createDomain($domain) {
		
		$dataTable = "scottlib_".$domain."_data";
		$catalogTable = "scottlib_".$domain."_catalog";
			
		try {
			$this->__conn->execute("
				CREATE TABLE IF NOT EXISTS `$dataTable` (
					`item_uniq` VARCHAR(32) NOT NULL,
					`attribute_name` VARCHAR(1024) CHARACTER SET 'utf8' NOT NULL,
					`value` VARCHAR(1024) CHARACTER SET 'utf8',
					INDEX (`item_uniq`),
					INDEX (`attribute_name`)
				) type=InnoDB;
			");
		} catch (Exception $e) {
			throw new Exception("Failed to create data table for [$domain]: ".$e->getMessage());	
		}
		
		try {
			$this->__conn->execute("
				CREATE TABLE IF NOT EXISTS `$catalogTable` (
					`uniq` VARCHAR(32) NOT NULL PRIMARY KEY,
					`item_name` VARCHAR(1024) NOT NULL,
					INDEX (`item_name`)
				) type=InnoDB;
			");
		} catch (Exception $e) {
			throw new Exception("Failed to create catalog table for [$domain]: ".$e->getMessage());	
		}
		
		return array($dataTable, $catalogTable);
		
	}
	
	function deleteDomain($domain) {
		
		$dataTable = "scottlib_".$domain."_data";
		$catalogTable = "scottlib_".$domain."_catalog";
			
		try {
			$this->__conn->execute("
				DROP TABLE IF EXISTS `$dataTable`
			");
		} catch (Exception $e) {
			throw new Exception("Failed to drop data table for [$domain]", $e);	
		}
		
		try {
			$this->__conn->execute("
				DROP TABLE IF EXISTS `$catalogTable`
			");
		} catch (Exception $e) {
			throw new Exception("Failed to drop catalog table for [$domain]", $e);	
		}
		
	}
	
	
	function put($domain, $itemName, $values = array()) {
		
		if (!$itemName)
			$itemName = md5(uniqid('scottlib'.$domain, true));
		
		$itemUniq = md5($itemName);	
		list ($dataTable, $catalogTable) = self::createDomain($domain);
			
		// prepare data manipulation:
		$deletes = array();
		$inserts = array();
		$this->generateSql($domain, $itemUniq, $values, $deletes, $inserts);
		
		$this->__conn->begin();

		// first, clear out any data being replaced:
		foreach($deletes as $d)
			$this->__conn->delete($d);
		
		// next, update the data stored for the item:
		if (count($inserts)) {
			$insert = "INSERT INTO `$dataTable` (`item_uniq`,`attribute_name`,`value`) VALUES ".join(",", $inserts);
			$this->__conn->insert($insert);
		}
		
		// finally, make sure item exists in the catalog
		$this->__conn->insert(
			slMySqlConn::prepare('REPLACE INTO # (`uniq`, `item_name`) VALUES (%s, %s)', array(
				$catalogTable, 
				$itemUniq, 
				$itemName
			))
		);

		$this->__conn->commit();
		
		return $itemName;
	}
	
	function delete($domain, $itemName) {}
	function get($domain, $id, $attributes) {}
	function meta($domain) {}
	function listDomains() {}
	function query($query) {}
	function select($select) {}
	function queryWithAttributes($domain, $attributes) {}
}

if (!function_exists('array_keys_all_numeric')) {
	function array_keys_all_numeric($arr) {
		return slMySqlEngine::allKeysAreNumeric($arr);	
	}
}

class slSimpleDbEngine  {
	
	static function numberEncode($value = null, $max_negative_value = SIMPLEDB_MAX_NEGATIVE_VALUE, $max_positive_value = SIMPLEDB_MAX_POSITIVE_VALUE) {
		if ($value === null)
			return null;
			
		if (!is_numeric($value))
			throw new Exception("Property $property_or_null is not numeric: $value");
		
		if ($value < $max_negative_value)
			throw new Exception("Value $value cannot be less than $max_negative_value");
		
		if ($value > $max_positive_value)
			throw new Exception("Value $value cannot be more than $max_positive_value");
			
		// first, pad the numeric value
		$value -= $max_negative_value;
		
		// next, pad the string value
		if (!preg_match('/(\d+)(\..*)/', $value, $matches)) {
			$integer = $value;
			$decimal = '';
		}
		else {
			$integer = $matches[1];
			$decimal = $matches[2];
		}
		
		while (strlen($integer) < strlen($max_positive_value))
			$integer = '0'.$integer;
			
		return $integer.($decimal ? $decimal : '');
	}
	
	static function numberDecode($value = null, $max_negative_value = SIMPLEDB_MAX_NEGATIVE_VALUE, $max_positive_value = SIMPLEDB_MAX_POSITIVE_VALUE) {
		if ($value === null)
			return null;
		
		// remove string padding
		$value = preg_replace('/^0+/', '', $value_or_object);
		
		// next, remove value padding
		$value += $max_negative_value;

		return $value;
	}
	
	static function date($value = null) {
		
		if ($value == null)
			$timestamp = time();
		else if (($timestamp = strtotime($value)) === false)
			throw new Exception("Failed to parse date value of [$value].");

		return date('c', $timestamp);
	}
	
	
}

if (!function_exists('slnumberencode')) {
	function slnumberencode($value, $max_negative_value = SIMPLEDB_MAX_NEGATIVE_VALUE, $max_positive_value = SIMPLEDB_MAX_POSITIVE_VALUE) {
		return slSimpleDbEngine::numberEncode($value, $max_negative_value, $max_positive_value);
	}
}

if (!function_exists('slnumberdecode')) {
	function slnumberdecode($value, $max_negative_value = SIMPLEDB_MAX_NEGATIVE_VALUE, $max_positive_value = SIMPLEDB_MAX_POSITIVE_VALUE) {
		return slSimpleDbEngine::numberDecode($value, $max_negative_value, $max_positive_value);
	}	
}

if (!function_exists('sldate')) {
	function sldate($value = null) {
		return slSimpleDbEngine::date($value);	
	}
}

// command line hook
if (isset($argv) && count($argv))
	require_once("scottlib-cmd.php");	
	
// echo 'scottlib loaded in '.(microtime()-$scottlib_include_at).'&#181;s';